﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Threading;
using System.Text;
using System.Text.RegularExpressions;
using System.Xml;
using AJAXChatClient.Flash.Net;

namespace AJAXChatClient
{
    using ComboBox = System.Windows.Forms.ComboBox;
    using HtmlDocument = System.Windows.Forms.HtmlDocument;
    using HtmlElement = System.Windows.Forms.HtmlElement;
    using HtmlElementCollection = System.Windows.Forms.HtmlElementCollection;
    using TextBox = System.Windows.Forms.TextBox;
    
    public delegate void Action();

    public class AjaxChat
    {
        private bool settingsInitiated;
        private bool styleInitiated;
        private Action initializeFunction;
        private Action finalizeFunction;
        private int? loginChannelID;
        private string loginChannelName;
        private int timerRate;
        private Timer timer;
        private string ajaxURL;
        private string baseURL;
        private string regExpMediaURL;
        private Dictionary<string, string> dirs;
        private bool startChatOnLoad;
        private bool chatStarted;
        private AjaxChatConfig.DOM_IDs domIDs;
        private Dictionary<string, HtmlElement> dom;
        private AjaxChatSettings settings;
        private string[] nonPersistentSettings;
        private AjaxChatSettings unusedSettings;
        private string[] bbCodeTags;
        private string[] colorCodes;
        private string[] emoticonCodes;
        private string[] emoticonFiles;

        private string[] userIcons;

        private AjaxChatConfig.Sound_Files soundFiles;
        private Dictionary<string, Flash.Media.Sound> sounds;
        private Flash.Media.SoundTransform soundTransform;
        private string sessionName;
        private int cookieExpiration;
        private string cookiePath;
        private string cookieDomain;
        private bool cookieSecure;
        private string chatBotName;
        private string encodedChatBotName;
        private int chatBotID;
        private bool allowUserMessageDelete;
        private int inactiveTimeout;
        private int privateChannelDiff;
        private int privateMessageDiff;
        private bool showChannelMessages;
        private int messageTextMaxLength;
        private bool socketServerEnabled;
        private string socketServerHost;
        private int socketServerPort;
        private int socketServerChatID;
        private Flash.Net.XMLSocket socket;
        private bool socketIsConnected;
        private int socketTimerRate;
        private Timer socketReconnectTimer;
        private string socketRegistrationID;
        private int userID;
        private string userName;
        private int userRole;
        private int channelID;
        private string channelName;
        private bool channelSwitch;
        private List<int> usersList = new List<int>();
        private List<string> userNamesList = new List<string>();
        private int userMenuCounter;
        private string encodedUserName;
        private string userNodeString;
        private List<string> ignoredUserNames;
        private string lastID;
        private int localID;
        private AjaxChatLang lang;
        private string langCode;
        private string baseDirection;
        private string originalDocumentTitle;
        private Timer blinkInterval;
        private HttpRequest[] httpRequests = new HttpRequest[51];
        private Timer retryTimer;
        private int retryTimerDelay;
        private string requestStatus = "ok";
        private bool DOMbuffering;
        private string DOMbuffer;
        private string DOMbufferRowClass = "rowOdd";

        private static HtmlDocument document;
        private static Cookie cookie;

        // inge tag - This is used for display when chat is listed.  This is the original way from the array
        // if usercolor is #0 is is regarded as unset and nothing happens - this will change to passing userID like icons
        private string getColorUserName(string userColor)
        {
            if (userColor == "#000000")
            {
                return null;
            }
            return userColor;
        }

        // inge tag - this is one of mine
        private string getColorUserID(int userID)
        {
            switch (userID)
            {
                case 2567: return "#CC1177";		// raspberry// Joska
                case    1: return "#E68CA3";		// pink 	// Inge
                case  708: return "#EE9500";		// orange 	// orangemittens
                case    6: return "#557700";		// bottle	// peter
                case 1088: return "#AA6600";		// russet	// Gurra
                case  267: return "#1C7BBD";		// blenblue	// cmo
                case    8: return "#996699";		// amethyst	// ingeli
                case 3183: return "#AA2233";		// blood	// Rick
                case 3566: return "#B0C241";		// lime		// recursor94
                case    4: return "#00A3B1";		// jade		// ghost
                case    5: return "#B09C6E";		// gold		// Bruce
                case 9999: return "#E19E87";		// salmon	// -
                case 2213: return "#88CC88";		// mint		// NonaMena
                case 2586: return "#A0ACDA";		// azure	// Consort
                case 2783: return "#D9BA00";		// banana	// Zedrael
                case 2296: return "#CC4422";		// cherry	// Cherry
                case  442: return "#000088";		// navy		// a
                case 2677: return "#009966";		// emerald	// Kuree
                case 1435: return "#887722";		// chocolate// ChaosMageX
                //default:			return "#000000";
            }
            return null;
        }

        // inge tag - get user icon
        private string getIconUserID(int userID)
        {
            if (Array.IndexOf(this.userIcons, userID) >= 0)
                return "<img src=\"" + this.baseURL + "img/usericons/" + userID + ".png\"></img> ";
            else
                return "<img src=\"" + this.baseURL + "img/usericons/blank.png\"></img> ";
        }

        /* NOT IN USE
        // inge tag - get colours around userName - this is the later way from persistent font color
        // getColorTaggedUsername('userName'),
        private string getColorTaggedUsername(string userName)
        {
            if (this.settings.PersistFontColor && this.settings.FontColor != null)
            {
                return "[color=" + this.settings.FontColor + "]" + userName + "[/color]";
            }
            return userName;
        }/* */

        private void init(AjaxChatConfig config, AjaxChatLang lang, bool initSettings, bool initStyle, bool initialize, Action initializeFunction, Action finalizeFunction)
        {
            this.httpRequests = new HttpRequest[51];
            this.usersList = new List<int>();
            this.userNamesList = new List<string>();
            this.userMenuCounter = 0;
            this.lastID = "0";
            this.localID = 0;
            this.lang = lang;
            this.initConfig(config);
            this.initDirectories();

            if (initSettings)
                this.initSettings();

            if (initStyle)
                this.initStyle();

            this.initializeFunction = initializeFunction;
            this.finalizeFunction = finalizeFunction;
            if (initialize)
                this.setLoadHandler();
        }

        private void initConfig(AjaxChatConfig config)
        {
            this.loginChannelID         = config.LoginChannelID;
            this.loginChannelName       = config.LoginChannelName;
            this.timerRate              = config.TimerRate;
            this.ajaxURL                = config.AjaxURL;
            this.baseURL                = config.BaseURL;
            this.regExpMediaURL         = config.RegExpMediaUrl;
            this.startChatOnLoad        = config.StartChatOnLoad;
            this.domIDs                 = config.DomIDs;
            this.settings               = config.Settings;
            this.nonPersistentSettings  = config.NonPersistentSettings;
            this.bbCodeTags             = config.BBCodeTags;
            this.colorCodes             = config.ColorCodes;
            this.emoticonCodes          = config.EmoticonCodes;
            this.emoticonFiles          = config.EmoticonFiles;
            this.userIcons              = config.UserIcons;
            this.soundFiles             = config.SoundFiles;
            this.sessionName            = config.SessionName;
            this.cookieExpiration       = config.CookieExpiration;
            this.cookiePath             = config.CookiePath;
            this.cookieDomain           = config.CookieDomain;
            this.cookieSecure           = config.CookieSecure;
            this.chatBotName            = config.ChatBotName;
            this.chatBotID              = config.ChatBotID;
            this.allowUserMessageDelete = config.AllowUserMessageDelete;
            this.inactiveTimeout        = config.InactiveTimeout;
            this.privateChannelDiff     = config.PrivateChannelDiff;
            this.privateMessageDiff     = config.PrivateMessageDiff;
            this.showChannelMessages    = config.ShowChannelMessages;
            this.messageTextMaxLength   = config.MessageTextMaxLength;
            this.socketServerEnabled    = config.SocketServerEnabled;
            this.socketServerHost       = config.SocketServerHost;
            this.socketServerPort       = config.SocketServerPort;
            this.socketServerChatID     = config.SocketServerChatID;
            this.DOMbuffering           = false;
            this.DOMbuffer              = "";
            this.retryTimerDelay        = (this.inactiveTimeout * 6000 - this.timerRate) / 4 + this.timerRate;
        }

        private void initDirectories()
        {
            this.dirs = new Dictionary<string, string>();
            this.dirs["emoticons"] = this.baseURL + "img/emoticons/";
            this.dirs["sounds"]    = this.baseURL + "sounds/";
            this.dirs["flash"]     = this.baseURL + "flash/";
        }

        private void initSettings()
        {
            /*this.settingsInitiated = true;
            this.unusedSettings = new AjaxChatSettings();
            string cookie = this.readCookie(this.sessionName + "_settings");
            if (cookie != null)
            {
                string[] settingsArray = cookie.Split('&');
                string[] setting;
                string key, val;
                object value;
                float number;
                int integer;
                for (int i = 0; i < settingsArray.Length; i++)
                {
                    setting = settingsArray[i].Split('=');
                    if (setting.Length == 2)
                    {
                        key = setting[0];
                        val = this.decodeText(setting[1]);
                        value = val;
                        switch (val)
                        {
                            case "true":
                                value = true;
                                break;
                            case "false":
                                value = false;
                                break;
                            case "null":
                                value = null;
                                break;
                            default:
                                if (float.TryParse(val, out number))
                                {
                                    if (int.TryParse(val, out integer))
                                        value = integer;
                                    else
                                        value = number;
                                }
                                break;
                        }

                        if (Array.IndexOf(this.nonPersistentSettings, key) >= 0)
                        {
                            // The setting is not used, store it for the persistSettings method:
                            this.unusedSettings[key] = value;
                        }
                        else
                        {
                            this.settings[key] = value;
                        }
                    }
                }
            }/* */
        }

        private void persistSettings()
        {
            /*if (this.settingsInitiated)
            {
                List<string> settingsArray = new List<string>();
                foreach (string property in this.settings.Keys)
                {
                    if (Array.IndexOf(nonPersistentSettings, property) >= 0)
                    {
                        if (this.unusedSettings != null && this.unusedSettings[property] != null)
                        {
                            // Store the unusedSetting previously stored:
                            this.settings[property] = this.unusedSettings[property];
                        }
                        else
                        {
                            continue;
                        }
                    }
                    settingsArray.Add(property + "=" + this.encodeText(this.settings[property].ToString()));
                }
                this.createCookie(this.sessionName + "_settings", string.Join("&", settingsArray.ToArray()), this.cookieExpiration);
            }/* */
        }

        private AjaxChatSettings getSettings()
        {
            return this.settings;
        }

        private object getSetting(string key)
        {
            // Only return null if setting is null or undefined, not if it is false:
            /*foreach (string property in this.settings.Keys)
            {
                if (property == key)
                {
                    return this.settings[key];
                }
            }/* */
            return null;
        }

        private void setSetting(string key, object value)
        {
            //this.settings[key] = value;
        }

        private void initializeSettings()
        {
            if (this.settings.PersistFontColor && this.settings.FontColor != null)
            {
                // Set the inputField font color to the font color:
                if (this.dom["inputField"] != null)
                {
                    HtmlHelpers.SetStyle(this.dom["inputField"], "color", this.settings.FontColor);
                }
            }
        }

        private void initialize()
        {
            this.setUnloadHandler();
            this.initializeDocumentNodes();
            this.loadPageAttributes();
            this.initEmoticons();
            this.initColorCodes();
            this.initializeSettings();
            this.setSelectedStyle();
            this.customInitialize();
            //preload the Alert icon (it can't display if there's no connection unless it's cached!)
            this.setStatus("retrying");
            if (this.initializeFunction != null)
            {
                this.initializeFunction();
            }
            if (!this.isCookieEnabled())
            {
                this.addChatBotMessageToChatList("/error CookiesRequired");
            }
            else
            {
                if (this.startChatOnLoad)
                {
                    this.startChat();
                }
                else
                {
                    this.setStartChatHandler();
                    this.requestTeaserContent();
                }
            }
        }

        private void requestTeaserContent()
        {
            string parameters = "&view=teaser";
            parameters += "&getInfos=" + AjaxChatHelpers.EncodeText("userID,userName,userRole");
            if (this.loginChannelID != null)
            {
                parameters += "&channelID=" + this.loginChannelID;
            }
            if (this.loginChannelName != null)
            {
                parameters += "&channelName=" + AjaxChatHelpers.EncodeText(this.loginChannelName);
            }
            this.updateChat(parameters);
        }

        private void setStartChatHandler()
        {
            if (this.dom["inputField"] != null)
            {
                /*this.dom["inputField"].onFocus = new Action(() => {
                    this.startChat();
                    // Reset the onfocus event on first call:
                    this.dom["inputField"].onFocus = null;
                });/* */
            }
        }

        private void startChat()
        {
            this.chatStarted = true;
            if (this.dom["inputField"] != null && this.settings.AutoFocus)
            {
                this.dom["inputField"].Focus();
            }
            this.loadFlashInterface();
            this.startChatUpdate();
        }

        private void loadPageAttributes()
        {
            HtmlElement htmlTag = document.GetElementsByTagName("html")[0];
            this.langCode      = !string.IsNullOrEmpty(htmlTag.GetAttribute("lang")) ? htmlTag.GetAttribute("lang") : "en";
            this.baseDirection = !string.IsNullOrEmpty(htmlTag.GetAttribute("dir"))  ? htmlTag.GetAttribute("dir")  : "ltr";
        }

        private void setLoadHandler()
        {
            // Make sure initialize() is called on page load:
  		    /*var onload = window.onload;
		    if (typeof onload != 'function') {
			    window.onload = function() {
				    ajaxChat.initialize();
			    }
		    } else {
			    window.onload = function() {
				    onload();
				    ajaxChat.initialize();
			    }
		    }/* */
        }

        private void setUnloadHandler()
        {
            // Make sure finalize() is called on page unload:
  		    /*var onunload = window.onunload;
		    if (typeof onunload != 'function') {
			    window.onunload = function() {
				    ajaxChat.finalize();
			    }
		    } else {
			    window.onunload = function() {
				    ajaxChat.finalize();
				    onunload();
			    }
		    }/* */
        }

        private void updateDOM(string id, string str, bool prepend, bool overwrite)
        {
            HtmlElement domNode;
            if (!this.dom.TryGetValue(id, out domNode))
            {
                domNode = document.GetElementById(id);
            }
            if (domNode == null)
            {
                return;
            }
            try
            {
                // Test for validity before adding the string to the DOM:
                //HtmlHelpers.CloneNode(domNode, false).InnerHtml = str;
                if (overwrite)
                    domNode.InnerHtml = str;
                else if (prepend)
                    domNode.InnerHtml = str + domNode.InnerHtml;
                else
                    domNode.InnerHtml += str;
            }
            catch (Exception)
            {
                this.addChatBotMessageToChatList("/error DOMSyntax " + id);
                this.updateChatlistView();
            }
        }

        private void initializeDocumentNodes()
        {
            this.dom = new Dictionary<string, HtmlElement>();
            foreach (string key in AjaxChatConfig.DOM_IDs.GetKeys())
            {
                this.dom[key] = document.GetElementById(this.domIDs.GetID(key));
            }
        }

        private void initEmoticons()
        {
            this.DOMbuffer = "";
            for (int i = 0; i < this.emoticonCodes.Length; i++)
            {
                // Replace special characters in emoticon codes:
                this.emoticonCodes[i] = AjaxChatHelpers.EncodeSpecialChars(this.emoticonCodes[i]);
                this.DOMbuffer = this.DOMbuffer
                    + "<a href=\"javascript:ajaxChat.insertText('"
                    + AjaxChatHelpers.ScriptLinkEncode(this.emoticonCodes[i])
                    + "');\"><img src=\""
                    + this.dirs["emoticons"]
                    + this.emoticonFiles[i]
                    + "\" alt=\""
                    + this.emoticonCodes[i]
                    + "\" title=\""
                    + this.emoticonCodes[i]
                    + "\"></a>";
            }
            if (this.dom["emoticonsContainer"] != null)
                this.updateDOM("emoticonsContainer", this.DOMbuffer, false, false);
            this.DOMbuffer = "";
        }

        private void initColorCodes()
        {
            if (this.dom["colorCodesContainer"] != null)
            {
                this.DOMbuffer = "";
                for (int i = 0; i < this.colorCodes.Length; i++)
                {
                    this.DOMbuffer = this.DOMbuffer
                        + "<a href=\"javascript:ajaxChat.setFontColor('"
                        + this.colorCodes[i]
                        + "');\" style=\"background-color:"
                        + this.colorCodes[i]
                        + ";\" title=\""
                        + this.colorCodes[i]
                        + "\"></a>"
                        + "\n";
                }
                this.updateDOM("colorCodesContainer", this.DOMbuffer, false, false);
                this.DOMbuffer = "";
            }
        }

        private void loadFlashInterface()
        {
            if (this.dom["flashInterfaceContainer"] != null)
            {
                this.updateDOM(
                    "flashInterfaceContainer",
                    "<object id=\"ajaxChatFlashInterface\" style=\"position:absolute; left:-100px;\" "
                    + "classid=\"clsid:D27CDB6E-AE6D-11cf-96B8-444553540000\" "
                    + "codebase=\""
                    //+ window.location.protocol
                    + "//download.macromedia.com/pub/shockwave/cabs/flash/swflash.cab#version=9,0,0,0\" "
                    + "height=\"1\" width=\"1\">"
                    + "<param name=\"flashvars\" value=\"bridgeName=ajaxChat\"/>"
                    + "<param name=\"src\" value=\"" + this.dirs["flash"] + "FABridge.swf\"/>"
                    + "<embed name=\"ajaxChatFlashInterface\" type=\"application/x-shockwave-flash\" pluginspage=\""
                    //+ window.location.protocol
                    + "//www.macromedia.com/go/getflashplayer\" "
                    + "src=\"" + this.dirs["flash"] + "FABridge.swf\" height=\"1\" width=\"1\" flashvars=\"bridgeName=ajaxChat\"/>"
                    + "</object>",
                    false, false
                );
                //FABridge.addInitializationCallback("ajaxChat", this.flashInterfaceLoadCompleteHandler);
            }
        }

        private void flashInterfaceLoadCompleteHandler()
        {
            this.initializeFlashInterface();
        }

        private void initializeFlashInterface()
        {
            if (this.socketServerEnabled)
            {
                this.socketTimerRate = (this.inactiveTimeout - 1) * 60 * 1000;
                this.socketConnect(null);
            }
            this.loadSounds();
            this.initializeCustomFlashInterface();
        }

        #region Socket Functions

        private void socketConnect(object state)
        {
            if (!this.socketIsConnected)
            {
                try
                {
                    if (this.socket == null)
                    {
                        this.socket = new Flash.Net.XMLSocket();
                        this.socket.ConnectEvent += new XMLSocket.ConnectEventHandler(this.socketConnectHandler);
                        this.socket.CloseEvent += new XMLSocket.CloseEventHandler(this.socketCloseHandler);
                        this.socket.DataEvent += new XMLSocket.DataEventHandler(this.socketDataHandler);
                        this.socket.IOErrorEvent += new XMLSocket.IOErrorEventHandler(this.socketIOErrorHandler);
                        this.socket.SecurityErrorEvent += new XMLSocket.SecurityErrorEventHandler(this.socketSecurityErrorHandler);

                    }
                    this.socket.Connect(this.socketServerHost, this.socketServerPort);
                }
                catch (Exception)
                {
                }
            }
            if (this.socketReconnectTimer != null)
            {
                this.socketReconnectTimer.Dispose();
            }
            this.socketReconnectTimer = null;
        }

        private void socketConnectHandler(XMLSocket.ConnectEventArgs e)
        {
            this.socketIsConnected = true;
            // setTimeout is needed to avoid calling the flash interface recursively:
            Timer timer = new Timer(this.socketRegister, null, 0, Timeout.Infinite);
        }

        private void socketCloseHandler(XMLSocket.CloseEventArgs e)
        {
            this.socketIsConnected = false;
            if (this.socket != null)
            {
                if (this.timer != null)
                {
                    this.timer.Dispose();
                    this.timer = null;
                }
                this.updateChat(null);
            }
        }

        private void socketDataHandler(XMLSocket.DataEventArgs e)
        {
            this.socketUpdate(e.Data.ToString());
        }

        private void socketIOErrorHandler(XMLSocket.IOErrorEventArgs e)
        {
            // setTimeout is needed to avoid calling the flash interface recursively (e.g. sound on new messages):
            Timer t1 = new Timer((state) => { this.addChatBotMessageToChatList("/error SocketIO"); }, null, 0, Timeout.Infinite);
            Timer t2 = new Timer((state) => { this.updateChatlistView(); }, null, 1, Timeout.Infinite);
        }

        private void socketSecurityErrorHandler(XMLSocket.SecurityErrorEventArgs e)
        {
            // setTimeout is needed to avoid calling the flash interface recursively (e.g. sound on new messages):
            Timer t1 = new Timer((state) => { this.addChatBotMessageToChatList("/error SocketSecurity"); }, null, 0, Timeout.Infinite);
            Timer t2 = new Timer((state) => { this.updateChatlistView(); }, null, 1, Timeout.Infinite);
        }

        private void socketRegister(object state)
        {
            if (this.socket != null && this.socketIsConnected)
            {
                try
                {
                    this.socket.Send(
                        "<register chatID=\""
                        + this.socketServerChatID
                        + "\" userID=\""
                        + this.userID
                        + "\" regID=\""
                        + this.socketRegistrationID
                        + "\"/>"
                    );
                }
                catch (Exception)
                {
                }
            }
        }

        private XmlDocument loadXML(string str)
        {
            // All kinds of browser specific code is here in the Javascript version,
            // but not necessary in the C# implementation.
            XmlDocument doc = new XmlDocument();
            doc.LoadXml(str);
            return doc;
        }

        private void socketUpdate(string data)
        {
            XmlDocument xmlDoc = this.loadXML(data);
            if (xmlDoc != null)
            {
                this.handleOnlineUsers(xmlDoc.GetElementsByTagName("user"));
                // If the root node has the attribute "mode" set to "1" it is a channel message:
                if ((this.showChannelMessages || xmlDoc.FirstChild.Attributes["mode"].Value != "1") && !this.channelSwitch)
                {
                    int channelID;
                    if (int.TryParse(xmlDoc.FirstChild.Attributes["channelID"].Value, out channelID) &&
                        channelID == this.channelID ||
                        channelID == this.userID + this.privateMessageDiff
                        )
                    {
                        this.handleChatMessages(xmlDoc.GetElementsByTagName("message"));
                    }
                }
            }
        }

        #endregion

        #region Audio Functions

        private void setAudioVolume(string volumeStr)
        {
            float volume;
            if (float.TryParse(volumeStr, out volume))
            {
                if (volume < 0)
                    volume = 0.0f;
                else if (volume > 1)
                    volume = 1.0f;

                this.settings.AudioVolume = volume;
                try
                {
                    if (this.soundTransform != null)
                    {
                        this.soundTransform = new Flash.Media.SoundTransform();
                    }
                    this.soundTransform.Volume = volume;
                }
                catch (Exception)
                {
                }
            }
        }

        private void loadSounds()
        {
            try
            {
                this.setAudioVolume(this.settings.AudioVolume.ToString());
                this.sounds = new Dictionary<string, Flash.Media.Sound>();
                Flash.Media.Sound sound;
                Flash.Net.URLRequest urlRequest;
                foreach (string key in AjaxChatConfig.Sound_Files.Keys)
                {
                    sound = new Flash.Media.Sound();
                    sound.CompleteEvent += new Flash.Media.Sound.CompleteEventHandler(this.soundLoadCompleteHandler);
                    sound.IOErrorEvent += new Flash.Media.Sound.IOErrorEventHandler(this.soundIOErrorHandler);
                    urlRequest = new Flash.Net.URLRequest();
                    urlRequest.URL = this.dirs["sounds"] + this.soundFiles[key];
                    sound.Load(urlRequest);
                }
            }
            catch (Exception)
            {
            }
        }

        private void soundLoadCompleteHandler(Flash.Media.Sound.CompleteEventArgs e)
        {
            Flash.Media.Sound sound = e.Target as Flash.Media.Sound;
            foreach (string key in AjaxChatConfig.Sound_Files.Keys)
            {
                // Get the sound key by matching the sound URL with the sound filename:
                if ((new Regex(this.soundFiles[key])).IsMatch(sound.URL))
                {
                    // Add the loaded sound to the sounds list:
                    this.sounds[key] = sound;
                }
            }
        }

        private void soundIOErrorHandler(Flash.Media.Sound.IOErrorEventArgs e)
        {
            // setTimeout is needed to avoid calling the flash interface recursively (e.g. sound on new messages):
            Timer t1 = new Timer((state) => { this.addChatBotMessageToChatList("/error SoundIO"); }, null, 0, Timeout.Infinite);
            Timer t2 = new Timer((state) => { this.updateChatlistView(); }, null, 1, Timeout.Infinite);
        }

        private void soundPlayCompleteHandler(EventArgs e)
        {
            // soundChannel event "soundComplete"
        }

        private Flash.Media.SoundChannel playSound(string soundID)
        {
            if (this.sounds != null && this.sounds[soundID] != null)
            {
                try
                {
                    // play() parameters are
                    // startTime:Number (default = 0)
                    // loops:int (default = 0) and
                    // sndTransform:SoundTransform (default = null)
                    return this.sounds[soundID].Play(0, 0, this.soundTransform);
                }
                catch (Exception)
                {
                    return null;
                }
            }
            return null;
        }

        private void playSoundOnNewMessage(int userID, string messageText)
        {
            if (this.settings.Audio && this.sounds != null && this.lastID != null && !this.channelSwitch)
            {
                string[] messageParts = messageText.Split(new char[] { ' ' }, 1);
                if (userID == this.chatBotID)
                {
                    switch (messageParts[0])
                    {
                        case "/login":
                        case "/channelEnter":
                            this.playSound(this.settings.SoundEnter);
                            break;
                        case "/logout":
                        case "/channelLeave":
                        case "/kick":
                            this.playSound(this.settings.SoundLeave);
                            break;
                        case "/error":
                            this.playSound(this.settings.SoundError);
                            break;
                        default:
                            this.playSound(this.settings.SoundChatBot);
                            break;
                    }
                }
                else if (userID == this.userID)
                {
                    this.playSound(this.settings.SoundSend);
                }
                else
                {
                    switch (messageParts[0])
                    {
                        case "/privmsg":
                            this.playSound(this.settings.SoundSend);
                            break;
                        default:
                            this.playSound(this.settings.SoundReceive);
                            break;
                    }
                }
            }
        }

        private void fillSoundSelection(string selectionID, string selectedSound)
        {
            //HtmlElement selection = document.GetElementById(selectionID);
            ComboBox selection = null;
            object[] options = new object[7];
            options[0] = "";
            // Skip the first, empy selection:
            int i = 1;
            int selectedIndex = 0;
            foreach (string key in AjaxChatConfig.Sound_Files.Keys)
            {
                options[i] = key;
                if (key == selectedSound)
                {
                    selectedIndex = i;
                }
                i++;
            }
            selection.Items.Clear();
            selection.Items.AddRange(options);
            selection.SelectedIndex = selectedIndex;
        }

        #endregion

        #region Connection Loop

        private void setStatus(string newStatus)
        {
            // status options are: ok, retrying, waiting
            if (this.requestStatus != "retrying" || newStatus == "ok")
            {
                this.requestStatus = newStatus;
            }
            HtmlElement statusIcon = document.GetElementById("statusIconContainer");
            if (statusIcon != null)
            {
                switch (this.requestStatus)
                {
                    case "ok":
                        AjaxChatHelpers.SetClass(statusIcon, "statusContainerOff");
                        break;
                    case "waiting":
                        AjaxChatHelpers.SetClass(statusIcon, "statusContainerOn");
                        break;
                    case "retrying":
                        AjaxChatHelpers.SetClass(statusIcon, "statusContainerAlert");
                        break;
                }
            }
        }

        private void startChatUpdate()
        {
            // Start the chat update and retrieve current user and channel info and set the login channel:
		    string infos = "userID,userName,userRole,channelID,channelName";
		    if (this.socketServerEnabled) 
            {
			    infos += ",socketRegistrationID";
		    }
		    string parameters = "&getInfos=" + AjaxChatHelpers.EncodeText(infos);
            if (this.loginChannelID != null)
            {
                parameters += "&channelID=" + this.loginChannelID;
            }
            else if (!string.IsNullOrEmpty(this.loginChannelName))
            {
                parameters += "&channelName=" + AjaxChatHelpers.EncodeText(this.loginChannelName);
            }
            this.updateChat(parameters);
        }

        private void updateChat(object paramString)
        {
            string requestUrl = this.ajaxURL + "&lastID=" + this.lastID;
            if (paramString != null && paramString is string)
            {
                requestUrl += paramString as string;
            }
            this.makeRequest(requestUrl, "GET", null);
        }

        private void forceNewRequest(object status)
        {
            this.updateChat(null);
            this.setStatus("retrying");
        }

        private int requestIdentifier;

        private void makeRequest(string url, string method, string data)
        {
            this.setStatus("waiting");
            try
            {
                int id;
                if (data != null)
                {
                    // Create up to 50 HttpWebRequest objects:
                    this.requestIdentifier = this.requestIdentifier++ % 50;
                    id = this.requestIdentifier + 1;
                }
                else
                {
                    id = 0;
                }
                //if the response takes longer than retryTimerDelay to give an OK status, 
                //abort the connection and start again.
                this.retryTimer = new Timer(this.forceNewRequest, null, this.retryTimerDelay, Timeout.Infinite);
                HttpRequest httpRequest = this.httpRequests[id];
                if (httpRequest != null)
                {
                    httpRequest.Dispose();
                }
                this.httpRequests[id] = new HttpRequest(this, url, method, data);
            }
            catch (WebException ex)
            {
                this.handleRequestTimeout(data != null, ex);
            }
        }

        private class HttpRequest : IDisposable
        {
            private bool done;
            private bool dataSent;
            private AjaxChat owner;
            private HttpWebRequest httpRequest;

            public HttpRequest(AjaxChat owner, string url, string method, string data)
            {
                this.done = false;
                this.owner = owner;
                this.dataSent = data != null;
                this.httpRequest = WebRequest.Create(url) as HttpWebRequest;
                this.httpRequest.Method = method;
                if (method == "POST")
                {
                    this.httpRequest.ContentType = "application/x-www-form-urlencoded";
                }
                if (this.dataSent)
                {
                    Stream stream = this.httpRequest.GetRequestStream();
                    StreamWriter sw = new StreamWriter(stream);
                    sw.Write(data);
                    sw.Close();
                }
                this.httpRequest.BeginGetResponse(this.responseCallback, null);
            }

            private void responseCallback(IAsyncResult result)
            {
                if (!this.done)
                {
                    try
                    {
                        HttpWebResponse response
                            = this.httpRequest.EndGetResponse(result) as HttpWebResponse;
                        this.done = true;
                        this.owner.handleResponse(response);
                    }
                    catch (WebException ex)
                    {
                        this.done = true;
                        this.owner.handleRequestTimeout(this.dataSent, ex);
                    }
                }
            }

            public void Dispose()
            {
                this.httpRequest.Abort();
            }
        }

        private void handleRequestTimeout(bool dataSent, WebException ex)
        {
            if (this.timer != null)
            {
                this.timer.Dispose();
                this.timer = null;
            }
            if (dataSent)
            {
                switch (ex.Status)
                {
                    case WebExceptionStatus.Timeout:
                    case WebExceptionStatus.RequestCanceled:
                        this.addChatBotMessageToChatList("/error ConnectionTimeout");
                        break;
                    default:
                        this.addChatBotMessageToChatList("/error ConnectionFailure " + ex.Status);
                        break;
                }
                this.setStatus("retrying");
                this.updateChatlistView();
            }
            this.timer = new Timer(this.updateChat, null, this.timerRate, Timeout.Infinite);
        }

        private bool handleResponse(HttpWebResponse response)
        {
            XmlDocument xmlDoc = null;
            if (response.StatusCode == HttpStatusCode.OK)
            {
                if (this.retryTimer != null)
                {
                    this.retryTimer.Dispose();
                    this.retryTimer = null;
                }
                xmlDoc = new XmlDocument();
                xmlDoc.Load(response.GetResponseStream());
                this.setStatus("ok");
            }
            else
            {
                // Connection status 0 can be ignored
                if (response.StatusCode == (HttpStatusCode)0)
                {
                    this.setStatus("waiting");
                }
                else
                {
                    string status = ((int)response.StatusCode) + " " + response.StatusCode;
                    this.addChatBotMessageToChatList("/error ConnectionStatus " + status);
                    this.setStatus("retrying");
                }
                this.updateChatlistView();
                return false;
            }
            if (xmlDoc == null)
            {
                return false;
            }
            this.handleXML(xmlDoc);
            this.setChatUpdateTimer();
            return true;
        }

        private void setChatUpdateTimer()
        {
            if (this.timer != null)
            {
                this.timer.Dispose();
                this.timer = null;
            }
            if (this.chatStarted)
            {
                int timeout;
                if (this.socketIsConnected)
                {
                    timeout = this.socketTimerRate;
                }
                else
                {
                    timeout = this.timerRate;
                    if (this.socketServerEnabled && this.socketReconnectTimer == null)
                    {
                        // If the socket connection fails try to reconnect once in a minute:
                        this.socketReconnectTimer = new Timer(this.socketConnect, null, 60000, Timeout.Infinite);
                    }
                }
                this.timer = new Timer(this.updateChat, null, timeout, Timeout.Infinite);
            }
        }

        #endregion// Connection Loop

        #region Handle XML

        private void handleXML(XmlDocument xmlDoc)
        {
            this.handleInfoMessages(xmlDoc.GetElementsByTagName("info"));
            this.handleOnlineUsers(xmlDoc.GetElementsByTagName("user"));
            this.handleChatMessages(xmlDoc.GetElementsByTagName("message"));
            this.channelSwitch = false;
        }

        private void handleInfoMessages(XmlNodeList infoNodes)
        {
            string infoType, infoData;
            for (int i = 0; i < infoNodes.Count; i++)
            {
                infoType = infoNodes[i].Attributes["type"].Value;
                infoData = infoNodes[i].FirstChild == null ? "" : infoNodes[i].FirstChild.Value;
                this.handleInfoMessage(infoType, infoData);
            }
        }

        private void handleInfoMessage(string infoType, string infoData)
        {
            switch (infoType)
            {
                case "channelSwitch":
                    this.clearChatList();
                    this.clearOnlineUsersList();
                    this.setSelectedChannel(infoData);
                    this.channelName = infoData;
                    this.channelSwitch = true;
                    break;
                case "channelName":
                    this.setSelectedChannel(infoData);
                    this.channelName = infoData;
                    break;
                case "channelID":
                    this.channelID = int.Parse(infoData);
                    break;
                case "userID":
                    this.userID = int.Parse(infoData);
                    break;
                case "userName":
                    this.userName = infoData;
                    this.encodedUserName = AjaxChatHelpers.ScriptLinkEncode(this.userName);
                    this.userNodeString = null;
                    break;
                case "userRole":
                    this.userRole = int.Parse(infoData);
                    break;
                case "logout":
                    this.handleLogout(infoData);
                    return;
                case "socketRegistrationID":
                    this.socketRegistrationID = infoData;
                    this.socketRegister(null);
                    break;
                default:
                    this.handleCustomInfoMessage(infoType, infoData);
                    break;
            }
        }

        private void handleOnlineUsers(XmlNodeList userNodes)
        {
            if (userNodes.Count > 0)
            {
                int index, userID, userRole;
                string userName;
                List<int> onlineUsers = new List<int>(userNodes.Count);
                for (int i = 0; i < userNodes.Count; i++)
                {
                    userID = int.Parse(userNodes[i].Attributes["userID"].Value);
                    userName = userNodes[i].FirstChild == null ? "" : userNodes[i].FirstChild.Value;
                    userRole = int.Parse(userNodes[i].Attributes["userRole"].Value);
                    onlineUsers.Add(userID);
                    index = this.usersList.IndexOf(userID);
                    if (index == -1)
                    {
                        this.addUserToOnlineList(
                            userID,
                            userName,
                            userRole
                        );
                    }
                    else if (this.userNamesList[index] != userName)
                    {
                        this.removeUserFromOnlineList(userID, index);
                        this.addUserToOnlineList(
                            userID,
                            userName,
                            userRole
                        );
                    }
                }
                // Clear the offline users from the online users list
                for (int i = 0; i < this.usersList.Count; i++)
                {
                    if (onlineUsers.IndexOf(this.usersList[i]) < 0)
                    {
                        this.removeUserFromOnlineList(this.usersList[i], i);
                    }
                }
                this.setOnlineListRowClasses();
            }
        }

        private void handleChatMessages(XmlNodeList messageNodes)
        {
            if (messageNodes.Count > 0)
            {
                XmlAttributeCollection msgAttrs;
                XmlNode userNode, textNode;
                string userName, messageText;
                for (int i = 0; i < messageNodes.Count; i++)
                {
                    this.DOMbuffering = true;
                    userNode = XmlHelpers.GetFirstChildWithTagName(messageNodes[i], "username");
                    userName = userNode.FirstChild == null ? "" : userNode.FirstChild.Value;
                    textNode = XmlHelpers.GetFirstChildWithTagName(messageNodes[i], "text");
                    messageText = textNode.FirstChild == null ? "" : textNode.FirstChild.Value;
                    msgAttrs = messageNodes[i].Attributes;
                    if (i == (messageNodes.Count - 1)) this.DOMbuffering = false;
                    this.addMessageToChatList(
                        DateTime.Parse(msgAttrs["dateTime"].Value),
                        int.Parse(msgAttrs["userID"].Value),
                        userName,
                        int.Parse(msgAttrs["userRole"].Value),
                        msgAttrs["id"].Value,
                        messageText,
                        int.Parse(msgAttrs["channelID"].Value),
                        msgAttrs["ip"].Value
                    );
                }
                this.DOMbuffering = false;
                this.updateChatlistView();
                this.lastID = messageNodes[messageNodes.Count - 1].Attributes["id"].Value;
            }
        }

        private void setSelectedChannel(string channel)
        {
            ComboBox channelSelection = null;
            if (channelSelection != null)
            {
                // Replace the entities in the channel name with their character equivalent:
                channel = AjaxChatHelpers.DecodeSpecialChars(channel);
                int channelSelectedIndex = -1;
                ComboBox.ObjectCollection options = channelSelection.Items;
                for (int i = 0; i < options.Count; i++)
                {
                    if (options[i].ToString() == channel)
                    {
                        channelSelectedIndex = i;
                        break;
                    }
                }
                // The given channel is not in the list, add it:
                if (channelSelectedIndex == -1)
                {
                    /*HtmlElement option = document.CreateElement("option");
                    //XmlNode text = document.CreateTextNode(channel);
                    //option.AppendChild(text);
                    option.InnerText = channel;
                    option.SetAttribute("value", channel);
                    option.SetAttribute("selected", "selected");
                    this.dom["channelSelection"].AppendChild(option);/* */
                    options.Add(channel);
                    channelSelectedIndex = options.Count - 1;
                }
                channelSelection.SelectedIndex = channelSelectedIndex;
            }
        }

        private void removeUserFromOnlineList(int userID, int index)
        {
            this.usersList.RemoveAt(index);
            this.userNamesList.RemoveAt(index);
            if (this.dom["onlineList"] != null)
            {
                HtmlHelpers.RemoveChild(this.dom["onlineList"], this.getUserNode(userID));
            }
        }

        private void addUserToOnlineList(int userID, string userName, int userRole)
        {
            this.usersList.Add(userID);
            this.userNamesList.Add(userName);
            if (this.dom["onlineList"] != null)
            {
                this.updateDOM(
                    "onlineList",
                    this.getUserNodeString(userID, userName, userRole),
                    (this.userID == userID), false
                );
            }
        }

        private string getUserNodeString(int userID, string userName, int userRole)
        {
            if (this.userNodeString != null && userID == this.userID)
            {
                return this.userNodeString;
            }
            else
            {
                string encodedUserName = AjaxChatHelpers.ScriptLinkEncode(userName);
                string str = "<div id=\""
                    + this.getUserDocumentID(userID)
                    + "\"><a href=\"javascript:ajaxChat.toggleUserMenu('"
                    + this.getUserMenuDocumentID(userID)
                    + "', '"
                    + encodedUserName
                    + "', "
                    + userID
                    + ");\" class=\""
                    + this.getRoleClass(userRole)
                    + "\" title=\""
                    + this.lang.toggleUserMenu.Replace("%s", userName)
                    + "\">"
                    + userName
                    + "</a>"
                    + "<ul class=\"userMenu\" id=\""
                    + this.getUserMenuDocumentID(userID)
                    + "\""
                    + ((userID == this.userID) ?
                        //">" + this.getUserNodeStringItems(encodedUserName, userID, false) :
                        " style=\"display:none;\">" :
                        " style=\"display:none;\">")
                    + "</ul>"
                    + "</div>";
                if (userID == this.userID)
                {
                    this.userNodeString = str;
                }
                return str;
            }
        }

        #endregion// Handle XML

        private void toggleUserMenu(string menuID, string userName, int userID)
        {
            // If the menu is empty, fill it with user node menu items before toggling it.
            bool isInline = false;
            if (menuID.IndexOf("ium") >= 0)
            {
                isInline = true;
            }
            if (document.GetElementById(menuID).FirstChild == null)
            {
                this.updateDOM(
                    menuID,
                    this.getUserNodeStringItems(
                        AjaxChatHelpers.EncodeText(AjaxChatHelpers.AddSlashes(AjaxChatHelpers.GetScriptLinkValue(userName))),
                        userID,
                        isInline
                    ),
                    false,
                    true
                );
            }
            this.showHide(menuID, null, false);
            //this.dom["chatList"].scrollTop = this.dom["chatList"].scrollHeight;
        }

        private string getUserNodeStringItems(string encodedUserName, int userID, bool isInline)
        {
            string menu;
            if (encodedUserName != this.encodedUserName)
            {
                menu = "<li><a href=\"javascript:ajaxChat.insertMessageWrapper('/msg "
                    + encodedUserName
                    + " ');\">"
                    + this.lang.userMenuSendPrivateMessage
                    + "</a></li>"
                    + "<li><a href=\"javascript:ajaxChat.insertMessageWrapper('/describe "
                    + encodedUserName
                    + " ');\">"
                    + this.lang.userMenuDescribe
                    + "</a></li>"
                    + "<li><a href=\"javascript:ajaxChat.sendMessageWrapper('/query "
                    + encodedUserName
                    + "');\">"
                    + this.lang.userMenuOpenPrivateChannel
                    + "</a></li>"
                    + "<li><a href=\"javascript:ajaxChat.sendMessageWrapper('/query');\">"
                    + this.lang.userMenuClosePrivateChannel
                    + "</a></li>"
                    + "<li><a href=\"javascript:ajaxChat.sendMessageWrapper('/ignore "
                    + encodedUserName
                    + "');\">"
                    + this.lang.userMenuIgnore
                    + "</a></li>";
                if (isInline)
                {
                    menu += "<li><a href=\"javascript:ajaxChat.sendMessageWrapper('/invite "
                        + encodedUserName
                        + "');\">"
                        + this.lang.userMenuInvite
                        + "</a></li>"
                        + "<li><a href=\"javascript:ajaxChat.sendMessageWrapper('/uninvite "
                        + encodedUserName
                        + "');\">"
                        + this.lang.userMenuUninvite
                        + "</a></li>"
                        + "<li><a href=\"javascript:ajaxChat.sendMessageWrapper('/whereis "
                        + encodedUserName
                        + "');\">"
                        + this.lang.userMenuWhereis
                        + "</a></li>";
                }
                if (this.userRole == 2 || this.userRole == 3)
                {
                    menu += "<li><a href=\"javascript:ajaxChat.insertMessageWrapper('/kick "
                        + encodedUserName
                        + " ');\">"
                        + this.lang.userMenuKick
                        + "</a></li>"
                        + "<li><a href=\"javascript:ajaxChat.sendMessageWrapper('/whois "
                        + encodedUserName
                        + "');\">"
                        + this.lang.userMenuWhois
                        + "</a></li>";
                }
            }
            else
            {
                menu = "<li><a href=\"javascript:ajaxChat.sendMessageWrapper('/quit');\">"
                    + this.lang.userMenuLogout
                    + "</a></li>"
                    + "<li><a href=\"javascript:ajaxChat.sendMessageWrapper('/who');\">"
                    + this.lang.userMenuWho
                    + "</a></li>"
                    + "<li><a href=\"javascript:ajaxChat.sendMessageWrapper('/ignore');\">"
                    + this.lang.userMenuIgnoreList
                    + "</a></li>"
                    + "<li><a href=\"javascript:ajaxChat.sendMessageWrapper('/list');\">"
                    + this.lang.userMenuList
                    + "</a></li>"
                    + "<li><a href=\"javascript:ajaxChat.insertMessageWrapper('/action ');\">"
                    + this.lang.userMenuAction
                    + "</a></li>"
                    + "<li><a href=\"javascript:ajaxChat.insertMessageWrapper('/roll ');\">"
                    + this.lang.userMenuRoll
                    + "</a></li>"
                    + "<li><a href=\"javascript:ajaxChat.insertMessageWrapper('/nick ');\">"
                    + this.lang.userMenuNick
                    + "</a></li>";
                if (this.userRole == 1 || this.userRole == 2 || this.userRole == 3)
                {
                    menu += "<li><a href=\"javascript:ajaxChat.sendMessageWrapper('/join');\">"
                        + this.lang.userMenuEnterPrivateRoom
                        + "</a></li>";
                    if (this.userRole == 2 || this.userRole == 3)
                    {
                        menu += "<li><a href=\"javascript:ajaxChat.sendMessageWrapper('/bans');\">"
                            + this.lang.userMenuBans
                            + "</a></li>";
                    }
                }
            }
            menu += this.getCustomUserMenuItems(encodedUserName, userID);
            return menu;
        }

        private void setOnlineListRowClasses()
        {
            if (this.dom["onlineList"] != null)
            {
                HtmlElement node = this.dom["onlineList"].FirstChild;
                bool rowEven = false;
                while (node != null)
                {
                    AjaxChatHelpers.SetClass(node, (rowEven ? "rowEven" : "rowOdd"));
                    node = node.NextSibling;
                    rowEven = !rowEven;
                }
            }
        }

        private void clearChatList()
        {
            /*while (this.dom["chatList"].HasChildNodes)
            {
                this.dom["chatList"].RemoveChild(this.dom["chatList"].FirstChild);
            }/* */
            this.dom["chatList"].InnerHtml = "";//TODO: Will this work????
        }

        private void clearOnlineUsersList()
        {
            this.usersList.Clear();
            this.userNamesList.Clear();
            if (this.dom["onlineList"] != null)
            {
                /*while (this.dom["onlineList"].HasChildNodes)
                {
                    this.dom["onlineList"].RemoveChild(this.dom["onlineList"].FirstChild);
                }/* */
                this.dom["onlineList"].InnerHtml = "";//TODO: Will this work????
            }
        }

        private string getEncodedChatBotName()
        {
            if (this.encodedChatBotName == null)
            {
                this.encodedChatBotName = AjaxChatHelpers.EncodeSpecialChars(this.chatBotName);
            }
            return this.encodedChatBotName;
        }

        private void addChatBotMessageToChatList(string messageText)
        {
            this.addMessageToChatList(
                DateTime.Now,
                this.chatBotID,
                this.getEncodedChatBotName(),
                4,
                null,
                messageText,
                this.channelID,//null,
                null
            );
        }

        private void addMessageToChatList(DateTime dateObject, int userID, string userName, int userRole, string messageID, string messageText, int channelID, string ip)
        {
            // Prevent adding the same message twice:
            if (this.getMessageNode(messageID) != null)
            {
                return;
            }
            if (!this.onNewMessage(dateObject, userID, userName, userRole, messageID, messageText, channelID, ip))
            {
                return;
            }
            this.DOMbufferRowClass = this.DOMbufferRowClass == "rowEven" ? "rowOdd" : "rowEven";
            this.DOMbuffer = this.DOMbuffer + 
                this.getChatListMessageString(
                    dateObject, userID, userName, userRole, messageID, messageText, channelID, ip
                );
            if (!this.DOMbuffering)
            {
                this.updateDOM("chatList", this.DOMbuffer, false, false);
                this.DOMbuffer = "";
            }
        }

        // This is the output on screen!  :))   - Inge
        private string getChatListMessageString(DateTime dateObject, int userID, string userName, int userRole, string messageID, string messageText, int channelID, string ip)
        {
            string rowClass = this.DOMbufferRowClass;
            string userClass = this.getRoleClass(userRole);
            string colon;
            if (messageText.IndexOf("/action") == 0 || messageText.IndexOf("/me") == 0 || messageText.IndexOf("/privaction") == 0)
            {
                userClass += " action";
                colon = " ";
            }
            else
            {
                colon = ": ";
            }
            string dateTime = this.settings.DateFormat != null ? "<span class=\"dateTime\">"
                + AjaxChatHelpers.FormatDate(this.settings.DateFormat, dateObject) + "</span> " : "";

            return "<div id=\""
                + this.getMessageDocumentID(messageID)
                + "\" class=\""
                + rowClass
                + "\">"
                + this.getDeletionLink(messageID, userID, userRole, channelID)
                + dateTime
                + this.getIconUserID(userID)
                + "<span class=\""
                + userClass
                + '"'
                + " style=\"color:"
                + this.getColorUserID(userID)
                + ";\""
                + this.getChatListUserNameTitle(userID, userName, userRole, ip)
                + " dir=\""
                + this.baseDirection
                + "\" onclick=\"ajaxChat.insertText(this.firstChild.nodeValue);\">"
                + userName
                + "</span>"
                + colon
                + this.replaceText(messageText)
                + "</div>";
        }

        private string getChatListUserNameTitle(int userID, string userName, int userRole, string ip)
        {
            return (ip != null) ? " title=\"IP: " + ip + '"' : "";
        }

        private string getMessageDocumentID(string messageID)
        {
            return (messageID == null) ? "ajaxChat_lm_" + (this.localID++) : "ajaxChat_m_" + messageID;
        }

        private HtmlElement getMessageNode(string messageID)
        {
            return (messageID == null) ? null : document.GetElementById(this.getMessageDocumentID(messageID));
        }

        private string getUserDocumentID(int userID)
        {
            return "ajaxChat_u_" + userID;
        }

        private HtmlElement getUserNode(int userID)
        {
            return document.GetElementById(this.getUserDocumentID(userID));
        }

        private string getUserMenuDocumentID(int userID)
        {
            return "ajaxChat_um_" + userID;
        }

        private string getInlineUserMenuDocumentID(int menuID, int index)
        {
            return "ajaxChat_ium_" + menuID + "_" + index;
        }

        private string deleteMessageStr;

        private string getDeletionLink(string messageID, int userID, int userRole, int channelID)
        {
            if (messageID != null && this.isAllowedToDeleteMessage(messageID, userID, userRole, channelID))
            {
                if (this.deleteMessageStr == null)
                {
                    this.deleteMessageStr = AjaxChatHelpers.EncodeSpecialChars(this.lang.deleteMessage);
                }
                return "<a class=\"delete\" title=\""
                    + this.deleteMessageStr
                    + "\" href=\"javascript:ajaxChat.deleteMessage("
                    + messageID
                    + ");\"> </a>"; // Adding a space - without any content Opera messes up the chatlist display
            }
            return "";
        }

        private bool isAllowedToDeleteMessage(string messageID, int userID, int userRole, int channelID)
        {
            if ((((this.userRole == 1 && this.allowUserMessageDelete && (userID == this.userID ||
                channelID == this.userID + this.privateMessageDiff ||
                channelID == this.userID + this.privateChannelDiff)) ||
                this.userRole == 2) && userRole != 3 && userRole != 4) || this.userRole == 3)
            {
                return true;
            }
            return false;
        }

        private bool onNewMessage(DateTime dateObject, int userID, string userName, int userRole, string messageID, string messageText, int channelID, string ip)
        {
            if (!this.customOnNewMessage(dateObject, userID, userName, userRole, messageID, messageText, channelID, ip))
            {
                return false;
            }
            if (this.ignoreMessage(userID, userName, messageText))
            {
                return false;
            }
            if (this.parseDeleteMessageCommand(messageText))
            {
                return false;
            }
            this.blinkOnNewMessage(userID, userName);
            this.playSoundOnNewMessage(userID, messageText);
            return true;
        }

        private bool parseDeleteMessageCommand(string messageText)
        {
            if (messageText.IndexOf("/delete") == 0)
            {
                string messageID = messageText.Substring(8);
                HtmlElement messageNode = this.getMessageNode(messageID);
                if (messageNode != null)
                {
                    HtmlElement nextSibling = messageNode.NextSibling;
                    try
                    {
                        HtmlHelpers.RemoveChild(this.dom["chatList"], messageNode);
                        if (nextSibling != null)
                        {
                            this.updateChatListRowClasses(nextSibling, messageNode);
                        }
                    }
                    catch (Exception)
                    {
                    }
                }
                return true;
            }
            return false;
        }

        private void blinkOnNewMessage(int userID, string userName)
        {
            if (this.settings.Blink && this.lastID != null && !this.channelSwitch && userID != this.userID)
            {
                if (this.blinkInterval != null)
                {
                    this.blinkInterval.Dispose();
                    this.blinkInterval = null;
                }
                int blink = this.settings.BlinkInterval;
                this.blinkInterval = new Timer((state) => {
                    this.blinkUpdate(AjaxChatHelpers.AddSlashes(AjaxChatHelpers.DecodeSpecialChars(userName))); },
                    null, blink, blink);
            }
        }

        private int blink;

        private void blinkUpdate(string blinkStr)
        {
            if (this.originalDocumentTitle == null)
            {
                this.originalDocumentTitle = document.Title;
            }
            if (this.blink == 0)
            {
                document.Title = "[@ ] " + blinkStr + " - " + this.originalDocumentTitle;
                this.blink = 1;
            }
            else if (this.blink > (int)this.settings.BlinkIntervalNumber)
            {
                if (this.blinkInterval != null)
                {
                    this.blinkInterval.Dispose();
                    this.blinkInterval = null;
                }
                document.Title = this.originalDocumentTitle;
                this.blink = 0;
            }
            else
            {
                if (this.blink % 2 != 0)
                {
                    document.Title = "[@ ]" + blinkStr + " - " + this.originalDocumentTitle;
                }
                else
                {
                    document.Title = "[ @]" + blinkStr + " - " + this.originalDocumentTitle;
                }
                this.blink++;
            }
        }

        private void updateChatlistView()
        {
            HtmlElementCollection children = this.dom["chatList"].Children;
            if (children.Count > 0 && this.settings.MaxMessages > 0 &&
                children.Count > this.settings.MaxMessages)
            {
                StringBuilder sb = new StringBuilder();
                for (int i = children.Count - this.settings.MaxMessages; i < children.Count; i++)
                {
                    sb.Append(children[i].OuterHtml);
                }
                this.dom["chatList"].InnerHtml = sb.ToString();
                /*while (children.Count > this.settings.MaxMessages)
                {
                    this.dom["chatList"].RemoveChild(this.dom["chatList"].FirstChild);
                }/* */
            }
        }

        private int? getUserIDFromUserName(string userName)
        {
            int index = this.userNamesList.IndexOf(userName);
            if (index >= 0)
            {
                return this.usersList[index];
            }
            return null;
        }

        private string getUserNameFromUserID(int userID)
        {
            int index = this.usersList.IndexOf(userID);
            if (index >= 0)
            {
                return this.userNamesList[index];
            }
            return null;
        }

        private string getRoleClass(int roleID)
        {
            switch (roleID)
            {
                case 0:
                    return "guest";
                case 1:
                    return "user";
                case 2:
                    return "moderator";
                case 3:
                    return "admin";
                case 4:
                    return "chatBot";
                default:
                    return "default";
            }
        }

        private bool handleInputFieldKeyPress(System.Windows.Forms.KeyEventArgs e)
        {
            if (e.KeyCode == System.Windows.Forms.Keys.Enter && !e.Shift)
            {
                this.sendMessage(null);
                e.Handled = true;
                return false;
            }
            return true;
        }

        private void handleInputFieldKeyUp(System.Windows.Forms.KeyEventArgs e)
        {
            this.updateMessageLengthCounter();
        }

        private void updateMessageLengthCounter()
        {
            if (this.dom["messageLengthCounter"] != null)
            {
                this.updateDOM(
                    "messageLengthCounter",
                    this.dom["inputField"].InnerText.Length + "/" + this.messageTextMaxLength,
                    false,
                    true
                );
            }
        }

        private void sendMessage(string text)
        {
            text = text != null ? text : this.dom["inputField"].InnerText;
            if (text == null)
            {
                return;
            }
            text = this.parseInputMessage(text);
            if (text != null)
            {
                if (this.timer != null)
                {
                    this.timer.Dispose();
                    this.timer = null;
                }
                string message = "lastID="
                    + this.lastID
                    + "&text="
                    + AjaxChatHelpers.EncodeText(text);
                this.makeRequest(this.ajaxURL, "POST", message);
            }
            this.dom["inputField"].InnerText = "";
            this.dom["inputField"].Focus();
            this.updateMessageLengthCounter();
        }

        private string parseInputMessage(string text)
        {
            if (text[0] == '/')
            {
                string[] textParts = text.Split(' ');
                switch (textParts[0])
                {
                    case "/ignore":
                        text = this.parseIgnoreInputCommand(text, textParts);
                        break;
                    case "/clear":
                        this.clearChatList();
                        return null;
                    default:
                        text = this.parseCustomInputCommand(text, textParts);
                        break;
                }
                if (text != null && this.settings.PersistFontColor && this.settings.FontColor != null)
                {
                    text = this.assignFontColorToCommandMessage(text, textParts);
                }
            }
            else
            {
                text = this.parseCustomInputMessage(text);
                if (text != null && this.settings.PersistFontColor && this.settings.FontColor != null)
                {
                    text = this.assignFontColorToMessage(text);
                }
            }
            return text;
        }

        private string assignFontColorToMessage(string text)
        {
            return "[color=" + this.settings.FontColor + "]" + text + "[/color]";
        }

        private string assignFontColorToCommandMessage(string text, string[] textParts)
        {
            switch (textParts[0])
            {
                case "/msg":
                case "/describe":
                    if (textParts.Length > 2)
                    {
                        return textParts[0] + " " + textParts[1] + " "
                            + "[color=" + this.settings.FontColor + "]"
                            + string.Join(" ", textParts, 2, textParts.Length - 2)
                            + "[/color]";
                    }
                    break;
                case "/me":
                case "/action":
                    if (textParts.Length > 1)
                    {
                        return textParts[0] + " "
                            + "[color=" + this.settings.FontColor + "]"
                            + string.Join(" ", textParts, 1, textParts.Length - 1)
                            + "[/color]";
                    }
                    break;
            }
            return text;
        }

        // !!! POTENTIAL ERROR: Shouldn't it be checking against this.encodedUserName instead of this.userName?
        private string parseIgnoreInputCommand(string text, string[] textParts)
        {
            List<string> ignoredUserNames = this.getIgnoredUserNames();
            if (textParts.Length > 1)
            {
                string userName = AjaxChatHelpers.EncodeSpecialChars(textParts[1]);
                // Prevent adding the chatBot or current user to the list:
                if (userName == this.userName || userName == this.getEncodedChatBotName())
                {
                    // Display the list of ignored users instead:
                    return this.parseIgnoreInputCommand(null, new string[] { "/ignore" });
                }
                if (ignoredUserNames.Count > 0)
                {
                    for (int i = ignoredUserNames.Count - 1; i >= 0; i--)
                    {
                        if (ignoredUserNames[i] == userName)
                        {
                            ignoredUserNames.RemoveAt(i);
                            this.addChatBotMessageToChatList("/ignoreRemoved " + userName);
                            this.setIgnoredUserNames(ignoredUserNames);
                            this.updateChatlistView();
                            return null;
                        }
                    }
                }
                ignoredUserNames.Add(userName);
                this.addChatBotMessageToChatList("/ignoreAdded " + userName);
                this.setIgnoredUserNames(ignoredUserNames);
            }
            else
            {
                if (ignoredUserNames.Count == 0)
                {
                    this.addChatBotMessageToChatList("/ignoreListEmpty -");
                }
                else
                {
                    this.addChatBotMessageToChatList("/ignoreList " + string.Join(" ", ignoredUserNames.ToArray()));
                }
            }
            this.updateChatlistView();
            return null;
        }

        private List<string> getIgnoredUserNames()
        {
            if (this.ignoredUserNames == null)
            {
                string ignoredUserNamesString = this.getSetting("ignoredUserNames") as string;
                if (ignoredUserNamesString != null)
                {
                    this.ignoredUserNames = new List<string>(ignoredUserNamesString.Split(' '));
                }
                else
                {
                    this.ignoredUserNames = new List<string>();
                }
            }
            return this.ignoredUserNames;
        }

        private void setIgnoredUserNames(List<string> ignoredUserNames)
        {
            this.ignoredUserNames = ignoredUserNames;
            this.setSetting("ignoredUserNames", string.Join(" ", ignoredUserNames.ToArray()));
        }

        private bool ignoreMessage(int userID, string userName, string messageText)
        {
            if (userID == this.chatBotID && messageText[0] == '/')
            {
                string[] textParts = messageText.Split(' ');
                if (textParts.Length > 1)
                {
                    switch (textParts[0])
                    {
                        case "/invite":
                        case "/uninvite":
                        case "/roll":
                            userName = textParts[1];
                            break;
                    }
                }
            }
            if (this.getIgnoredUserNames().IndexOf(userName) >= 0)
            {
                return true;
            }
            return false;
        }

        private void deleteMessage(string messageID)
        {
            HtmlElement messageNode = this.getMessageNode(messageID);
            if (messageNode != null)
            {
                string originalClass = AjaxChatHelpers.GetClass(messageNode);
                AjaxChatHelpers.SetClass(messageNode, originalClass + " deleteSelected");
                // TODO: Prompt User
                System.Windows.Forms.DialogResult confirm
                    = System.Windows.Forms.MessageBox.Show(
                    this.lang.deleteMessageConfirm, "Warning", 
                    System.Windows.Forms.MessageBoxButtons.OKCancel, 
                    System.Windows.Forms.MessageBoxIcon.Exclamation, 
                    System.Windows.Forms.MessageBoxDefaultButton.Button2);
                if (confirm == System.Windows.Forms.DialogResult.OK)
                {
                    HtmlElement nextSibling = messageNode.NextSibling;
                    try
                    {
                        HtmlHelpers.RemoveChild(this.dom["chatList"], messageNode);
                        if (nextSibling != null)
                        {
                            this.updateChatListRowClasses(nextSibling, messageNode);
                        }
                        this.updateChat("&delete=" + messageID);
                    }
                    catch (Exception)
                    {
                        AjaxChatHelpers.SetClass(messageNode, originalClass);
                    }
                }
                else
                {
                    AjaxChatHelpers.SetClass(messageNode, originalClass);
                }
            }
        }

        private void updateChatListRowClasses(HtmlElement node, HtmlElement previousNode)
        {
            if (node == null)
            {
                node = this.dom["chatList"].FirstChild;
                previousNode = null;
            }
            if (node != null)
            {
                //HtmlElement previousNode = HtmlHelpers.GetPreviousSibling(node);
                bool rowEven = previousNode != null && AjaxChatHelpers.GetClass(previousNode) == "rowOdd";
                while (node != null)
                {
                    AjaxChatHelpers.SetClass(node, (rowEven ? "rowEven" : "rowOdd"));
                    node = node.NextSibling;
                    rowEven = !rowEven;
                }
            }
        }

        private void sendMessageWrapper(string text)
        {
            this.sendMessage(AjaxChatHelpers.GetScriptLinkValue(text));
        }

        private void insertMessageWrapper(string text)
        {
            this.insertText(AjaxChatHelpers.GetScriptLinkValue(text), true);
        }

        private void switchChannel(string channel)
        {
            if (!this.chatStarted)
            {
                this.clearChatList();
                this.channelSwitch = true;
                this.loginChannelID = null;
                this.loginChannelName = channel;
                this.requestTeaserContent();
                return;
            }
            if (this.timer != null)
            {
                this.timer.Dispose();
                this.timer = null;
            }
            string message = "lastID="
                + this.lastID
                + "&channelName="
                + AjaxChatHelpers.EncodeText(channel);
            this.makeRequest(this.ajaxURL, "POST", message);
            if (this.dom["inputField"] != null && this.settings.AutoFocus)
            {
                this.dom["inputField"].Focus();
            }
        }

        private void logout()
        {
            if (this.timer != null)
            {
                this.timer.Dispose();
                this.timer = null;
            }
            string message = "logout=true";
            this.makeRequest(this.ajaxURL, "POST", message);
        }

        private void handleLogout(string url)
        {
            //window.location.href = url;
        }

        private void toggleSetting(string setting, string buttonID)
        {
            this.setSetting(setting, !(bool)this.getSetting(setting));
            if (buttonID != null)
            {
                this.updateButton(setting, buttonID);
            }
        }

        private void updateButton(string setting, string buttonID)
        {
            HtmlElement node = document.GetElementById(buttonID);
            if (node != null)
            {
                AjaxChatHelpers.SetClass(node, ((bool)this.getSetting(setting) ? "button" : "button off"));
            }
        }

        private void showHide(string id, string styleDisplay, bool displayInline)
        {
            HtmlElement node = document.GetElementById(id);
            if (node != null)
            {
                if (styleDisplay != null)
                {
                    //node.style.display = styleDisplay;
                    HtmlHelpers.SetStyle(node, "display", styleDisplay);
                }
                else
                {
                    if (/*node.style.display*/HtmlHelpers.GetStyle(node, "display") == "none")
                    {
                        //node.style.display = (displayInline ? "inline" : "block");
                        HtmlHelpers.SetStyle(node, "display", (displayInline ? "inline" : "block"));
                    }
                    else
                    {
                        //node.style.display = "none";
                        HtmlHelpers.SetStyle(node, "display", "none");
                    }
                }
            }
        }

        private void setPersistFontColor(bool boolean)
        {
            this.settings.PersistFontColor = boolean;
            if (!this.settings.PersistFontColor)
            {
                this.settings.FontColor = null;
                if (this.dom["inputField"] != null)
                {
                    HtmlHelpers.SetStyle(this.dom["inputField"], "color", "");
                }
            }
        }

        private void setFontColor(string color)
        {
            if (this.settings.PersistFontColor)
            {
                this.settings.FontColor = color;
                if (this.dom["inputField"] != null)
                {
                    HtmlHelpers.SetStyle(this.dom["inputField"], "color", color);
                }
                if (this.dom["colorCodesContainer"] != null)
                {
                    HtmlHelpers.SetStyle(this.dom["colorCodesContainer"], "display", "none");
                    if (this.dom["inputField"] != null)
                    {
                        this.dom["inputField"].Focus();
                    }
                }
            }
        }

        private void insertText(string text, bool clearInputField)
        {
            if (clearInputField)
            {
                this.dom["inputField"].InnerText = "";
            }
            this.insert(text, "");
        }

        private void insertBBCode(string bbCode)
        {
            switch (bbCode)
            {
                case "url":
                    string url = null;// TODO: Prompt User
                    if (url != null)
                        this.insert("[url=" + url + "]", "");
                    else
                        this.dom["inputField"].Focus();
                    break;
                default:
                    this.insert("[" + bbCode + "]", "[/" + bbCode + "]");
                    break;
            }
        }

        private void insert(string startTag, string endTag)
        {
            TextBox inputField = null;
            inputField.Focus();
            // Insert the tags:
            int start = inputField.SelectionStart;
            int end = start + inputField.SelectionLength;
            string insText = inputField.SelectedText;
            inputField.Text = inputField.Text.Substring(0, start)
                + startTag
                + insText
                + endTag
                + inputField.Text.Substring(end);
            // Adjust the cursor position:
            int pos;
            if (insText.Length == 0)
                pos = start + startTag.Length;
            else
                pos = start + startTag.Length + insText.Length + endTag.Length;
            inputField.SelectionStart = pos;
            inputField.SelectionLength = 0;
        }

        #region Replace Text

        private string replaceText(string text)
        {
            // inge added this if block as an option for saying something that will not be translated at all
            // linebreaks were considered non-negotiable though.  Could reconsider another time.
            if (text.Substring(0, 4) == "[nr]")
            {
                try
                {
                    text = this.replaceLineBreaks(text);
                }
                catch (Exception)
                {
                }
                return text;
            }
            try
            {
                text = this.replaceLineBreaks(text);
                if (text[0] == '/')
                {
                    text = this.replaceCommands(text);
                }
                else
                {
                    text = this.replaceBBCode(text);
                    text = this.replaceHyperLinks(text);
                    text = this.replaceEmoticons(text);
                }
                
                if (this.settings.WordWrap)
                {
                    // inge commented the following line 
                    // as it was placing unprintable chars into the strings 
                    // and breaking resourcekeys etc
                    //text = AjaxChatHelpers.BreakLongWords(text, this.settings.MaxWordLength);
                }
                text = this.replaceCustomText(text);
            }
            catch (Exception)
            {
            }
            return text;
        }

        #region Replace Commands

        private string replaceCommands(string text)
        {
            try
            {
                if (text[0] != '/')
                {
                    return text;
                }
                string[] textParts = text.Split(' ');
                switch (textParts[0])
                {
                    case "/login":
                        return this.replaceCommandLogin(textParts);
                    case "/logout":
                        return this.replaceCommandLogout(textParts);
                    case "/channelEnter":
                        return this.replaceCommandChannelEnter(textParts);
                    case "/channelLeave":
                        return this.replaceCommandChannelLeave(textParts);
                    case "/privmsg":
                        return this.replaceCommandPrivMsg(textParts);
                    case "/privmsgto":
                        return this.replaceCommandPrivMsgTo(textParts);
                    case "/privaction":
                        return this.replaceCommandPrivAction(textParts);
                    case "/privactionto":
                        return this.replaceCommandPrivActionTo(textParts);
                    case "/me":
                    case "/action":
                        return this.replaceCommandAction(textParts);
                    case "/invite":
                        return this.replaceCommandInvite(textParts);
                    case "/inviteto":
                        return this.replaceCommandInviteTo(textParts);
                    case "/uninvite":
                        return this.replaceCommandUninvite(textParts);
                    case "/uninviteto":
                        return this.replaceCommandUninviteTo(textParts);
                    case "/queryOpen":
                        return this.replaceCommandQueryOpen(textParts);
                    case "/queryClose":
                        return this.replaceCommandQueryClose(textParts);
                    case "/ignoreAdded":
                        return this.replaceCommandIgnoreAdded(textParts);
                    case "/ignoreRemoved":
                        return this.replaceCommandIgnoreRemoved(textParts);
                    case "/ignoreList":
                        return this.replaceCommandIgnoreList(textParts);
                    case "/ignoreListEmpty":
                        return this.replaceCommandIgnoreListEmpty(textParts);
                    case "/kick":
                        return this.replaceCommandKick(textParts);
                    case "/who":
                        return this.replaceCommandWho(textParts);
                    case "/whoChannel":
                        return this.replaceCommandWhoChannel(textParts);
                    case "/whoEmpty":
                        return this.replaceCommandWhoEmpty(textParts);
                    case "/list":
                        return this.replaceCommandList(textParts);
                    case "/bans":
                        return this.replaceCommandBans(textParts);
                    case "/bansEmpty":
                        return this.replaceCommandBansEmpty(textParts);
                    case "/unban":
                        return this.replaceCommandUnban(textParts);
                    case "/whois":
                        return this.replaceCommandWhois(textParts);
                    case "/whereis":
                        return this.replaceCommandWhereis(textParts);
                    case "/roll":
                        return this.replaceCommandRoll(textParts);
                    case "/nick":
                        return this.replaceCommandNick(textParts);
                    case "/error":
                        return this.replaceCommandError(textParts);
                    default:
                        return this.replaceCustomCommands(text, textParts);
                }
            }
            catch (Exception)
            {
            }
            return text;
        }

        private string replaceCommandLogin(string[] textParts)
        {
            return "<span class=\"chatBotMessage\">"
                + this.lang.login.Replace("%s", textParts[1])
                + "</span>";
        }

        private string replaceCommandLogout(string[] textParts)
        {
            string type = "";
            if (textParts.Length == 3)
                type = textParts[2];
            return "<span class=\"chatBotMessage\">"
                + this.lang.logoutMessage(type).Replace("%s", textParts[1])
                + "</span>";
        }

        private string replaceCommandChannelEnter(string[] textParts)
        {
            return "<span class=\"chatBotMessage\">"
                + this.lang.channelEnter.Replace("%s", textParts[1])
                + "</span>";
        }

        private string replaceCommandChannelLeave(string[] textParts)
        {
            return "<span class=\"chatBotMessage\">"
                + this.lang.channelLeave.Replace("%s", textParts[1])
                + "</span>";
        }

        private string replaceCommandPrivMsg(string[] textParts)
        {
            string privMsgText = string.Join(" ", textParts, 1, textParts.Length - 1);
            privMsgText = this.replaceBBCode(privMsgText);
            privMsgText = this.replaceHyperLinks(privMsgText);
            privMsgText = this.replaceEmoticons(privMsgText);
            return "<span class=\"privmsg\">"
                + this.lang.privmsg
                + "</span> "
                + privMsgText;
        }

        private string replaceCommandPrivMsgTo(string[] textParts)
        {
            string privMsgText = string.Join(" ", textParts, 2, textParts.Length - 2);
            privMsgText = this.replaceBBCode(privMsgText);
            privMsgText = this.replaceHyperLinks(privMsgText);
            privMsgText = this.replaceEmoticons(privMsgText);
            return "<span class=\"privmsg\">"
                + this.lang.privmsgto.Replace("%s", textParts[1])
                + "</span> "
                + privMsgText;
        }

        private string replaceCommandPrivAction(string[] textParts)
        {
            string privActionText = string.Join(" ", textParts, 1, textParts.Length - 1);
            privActionText = this.replaceBBCode(privActionText);
            privActionText = this.replaceHyperLinks(privActionText);
            privActionText = this.replaceEmoticons(privActionText);
            return "<span class=\"action\">"
                + privActionText
                + "</span> <span class=\"privmsg\">"
                + this.lang.privmsg
                + "</span> ";
        }

        private string replaceCommandPrivActionTo(string[] textParts)
        {
            string privActionText = string.Join(" ", textParts, 2, textParts.Length - 2);
            privActionText = this.replaceBBCode(privActionText);
            privActionText = this.replaceHyperLinks(privActionText);
            privActionText = this.replaceEmoticons(privActionText);
            return "<span class=\"action\">"
                + privActionText
                + "</span> <span class=\"privmsg\">"
                + this.lang.privmsgto.Replace("%s", textParts[1])
                + "</span> ";
        }

        private string replaceCommandAction(string[] textParts)
        {
            string actionText = string.Join(" ", textParts, 1, textParts.Length - 1);
            actionText = this.replaceBBCode(actionText);
            actionText = this.replaceHyperLinks(actionText);
            actionText = this.replaceEmoticons(actionText);
            return "<span class=\"action\">"
                + actionText
                + "</span>";
        }

        private string replaceCommandInvite(string[] textParts)
        {
            string inviteText = this.lang.invite;
            int index = inviteText.IndexOf("%s");
            inviteText = inviteText.Substring(0, index)
                + textParts[1]
                + inviteText.Substring(index + 2, inviteText.Length - index - 2);
            inviteText = inviteText.Replace("%s",
                "<a href=\"javascript:ajaxChat.sendMessageWrapper('/join "
                + AjaxChatHelpers.ScriptLinkEncode(textParts[2])
                + "');\" title=\""
                + this.lang.joinChannel.Replace("%s", textParts[2])
                + "\">"
                + textParts[2]
                + "</a>"
            );
            return "<span class=\"chatBotMessage\">"
                + inviteText
                + "</span>";
        }

        private string replaceCommandInviteTo(string[] textParts)
        {
            string inviteText = this.lang.inviteto;
            int index = inviteText.IndexOf("%s");
            inviteText = inviteText.Substring(0, index)
                + textParts[1]
                + inviteText.Substring(index + 2, inviteText.Length - index - 2);
            inviteText = inviteText.Replace("%s", textParts[2]);
            return "<span class=\"chatBotMessage\">"
                + inviteText
                + "</span>";
        }

        private string replaceCommandUninvite(string[] textParts)
        {
            string uninviteText = this.lang.uninvite;
            int index = uninviteText.IndexOf("%s");
            uninviteText = uninviteText.Substring(0, index)
                + textParts[1]
                + uninviteText.Substring(index + 2, uninviteText.Length - index - 2);
            uninviteText = uninviteText.Replace("%s", textParts[2]);
            return "<span class=\"chatBotMessage\">"
                + uninviteText
                + "</span>";
        }

        private string replaceCommandUninviteTo(string[] textParts)
        {
            string uninviteText = this.lang.uninviteto;
            int index = uninviteText.IndexOf("%s");
            uninviteText = uninviteText.Substring(0, index)
                + textParts[1]
                + uninviteText.Substring(index + 2, uninviteText.Length - index - 2);
            uninviteText = uninviteText.Replace("%s", textParts[2]);
            return "<span class=\"chatBotMessage\">"
                + uninviteText
                + "</span>";
        }

        private string replaceCommandQueryOpen(string[] textParts)
        {
            return "<span class=\"chatBotMessage\">"
                + this.lang.queryOpen.Replace("%s", textParts[1])
                + "</span>";
        }

        private string replaceCommandQueryClose(string[] textParts)
        {
            return "<span class=\"chatBotMessage\">"
                + this.lang.queryClose.Replace("%s", textParts[1])
                + "</span>";
        }

        private string replaceCommandIgnoreAdded(string[] textParts)
        {
            return "<span class=\"chatBotMessage\">"
                + this.lang.ignoreAdded.Replace("%s", textParts[1])
                + "</span>";
        }

        private string replaceCommandIgnoreRemoved(string[] textParts)
        {
            return "<span class=\"chatBotMessage\">"
                + this.lang.ignoreRemoved.Replace("%s", textParts[1])
                + "</span>";
        }

        private string replaceCommandIgnoreList(string[] textParts)
        {
            return "<span class=\"chatBotMessage\">"
                + this.lang.ignoreList + " "
                + this.getInlineUserMenu(textParts, 1)
                + "</span>";
        }

        private string replaceCommandIgnoreListEmpty(string[] textParts)
        {
            return "<span class=\"chatBotMessage\">"
                + this.lang.ignoreListEmpty
                + "</span>";
        }

        private string replaceCommandKick(string[] textParts)
        {
            return "<span class=\"chatBotMessage\">"
                + this.lang.logoutKicked.Replace("%s", textParts[1])
                + "</span>";
        }

        private string replaceCommandWho(string[] textParts)
        {
            return "<span class=\"chatBotMessage\">"
                + this.lang.who + " "
                + this.getInlineUserMenu(textParts, 1)
                + "</span>";
        }

        private string replaceCommandWhoChannel(string[] textParts)
        {
            return "<span class=\"chatBotMessage\">"
                + this.lang.whoChannel.Replace("%s", textParts[1]) + " "
                + this.getInlineUserMenu(textParts, 2)
                + "</span>";
        }

        private string replaceCommandWhoEmpty(string[] textParts)
        {
            return "<span class=\"chatBotMessage\">"
                + this.lang.whoEmpty
                + "</span>";
        }

        private string replaceCommandList(string[] textParts)
        {
            string[] listChannels = new string[textParts.Length - 1];
            string channelName;
            for (int i = 1; i < textParts.Length; i++)
            {
                channelName = (textParts[i] == this.channelName) ? "<b>" + textParts[i] + "</b>" : textParts[i];
                listChannels[i - 1] = "<a href=\"javascript:ajaxChat.sendMessageWrapper('/join "
                    + AjaxChatHelpers.ScriptLinkEncode(textParts[i])
                    + "');\" title=\""
                    + this.lang.joinChannel.Replace("%s", textParts[i])
                    + "\">"
                    + channelName
                    + "</a>";
            }
            return "<span class=\"chatBotMessage\">"
                + this.lang.list + " "
                + string.Join(", ", listChannels)
                + "</span>";
        }

        private string replaceCommandBans(string[] textParts)
        {
            string[] listUsers = new string[textParts.Length - 1];
            for (int i = 1; i < textParts.Length; i++)
            {
                listUsers[i - 1] = "<a href=\"javascript:ajaxChat.sendMessageWrapper('/unban "
                    + AjaxChatHelpers.ScriptLinkEncode(textParts[i])
                    + "');\" title=\""
                    + this.lang.unbanUser.Replace("%s", textParts[i])
                    + "\">"
                    + textParts[i]
                    + "</a>";
            }
            return "<span class=\"chatBotMessage\">"
                + this.lang.bans + " "
                + string.Join(", ", listUsers)
                + "</span>";
        }

        private string replaceCommandBansEmpty(string[] textParts)
        {
            return "<span class=\"chatBotMessage\">"
                + this.lang.bansEmpty
                + "</span>";
        }

        private string replaceCommandUnban(string[] textParts)
        {
            return "<span class=\"chatBotMessage\">"
                + this.lang.unban.Replace("%s", textParts[1])
                + "</span>";
        }

        private string replaceCommandWhois(string[] textParts)
        {
            return "<span class=\"chatBotMessage\">"
                + this.lang.whois.Replace("%s", textParts[1]) + " "
                + textParts[2]
                + "</span>";
        }

        private string replaceCommandWhereis(string[] textParts)
        {
            string whereis = this.lang.whereis;
            int index = whereis.IndexOf("%s");
            whereis = whereis.Substring(0, index)
                + textParts[1]
                + whereis.Substring(index + 2, whereis.Length - index - 2);
            whereis = whereis.Replace("%s",
                "<a href=\"javascript:ajaxChat.sendMessageWrapper('/join "
                + AjaxChatHelpers.ScriptLinkEncode(textParts[2])
                + "');\" title=\""
                + this.lang.joinChannel.Replace("%s", textParts[2])
                + "\">"
                + textParts[2]
                + "</a>"
            );
            return "<span class=\"chatBotMessage\">"
                + whereis
                + "</span>";
        }

        private string replaceCommandRoll(string[] textParts)
        {
            string rollText = this.lang.roll;
            int index = rollText.IndexOf("%s");
            rollText = rollText.Substring(0, index)
                + textParts[1]
                + rollText.Substring(index + 2, rollText.Length - index - 2);
            index = rollText.IndexOf("%s");
            rollText = rollText.Substring(0, index)
                + textParts[2]
                + rollText.Substring(index + 2, rollText.Length - index - 2);
            rollText.Replace("%s", textParts[3]);
            return "<span class=\"chatBotMessage\">"
                + rollText
                + "</span>";
        }

        private string replaceCommandNick(string[] textParts)
        {
            string nickText = this.lang.nick;
            int index = nickText.IndexOf("%s");
            nickText = nickText.Substring(0, index)
                + textParts[1]
                + nickText.Substring(index + 2, nickText.Length - index - 2);
            nickText.Replace("%s", textParts[2]);
            return "<span class=\"chatBotMessage\">"
                + nickText
                + "</span>";
        }

        private string replaceCommandError(string[] textParts)
        {
            string errorMessage = this.lang.errorMessage(textParts[1]);
            if (errorMessage == null)
            {
                errorMessage = "Error: Unknown.";
            }
            else if (textParts.Length > 2)
            {
                errorMessage = errorMessage.Replace("%s", string.Join(" ", textParts, 2, textParts.Length - 2));
            }
            return "<span class=\"chatBotMessage\">"
                + errorMessage
                + "</span>";
        }

        private string getInlineUserMenu(string[] users, int startIndex)
        {
            string menu = "";
            for (int i = startIndex; i < users.Length; i++)
            {
                if (i > startIndex)
                {
                    menu += ", ";
                }
                menu += "<a href=\"javascript:ajaxChat.toggleUserMenu('"
                    + this.getInlineUserMenuDocumentID(this.userMenuCounter, i)
                    + "', '"
                    + AjaxChatHelpers.ScriptLinkEncode(users[i])
                    + "', null);\" title=\""
                    + this.lang.toggleUserMenu.Replace("%s", users[i])
                    + "\" dir=\""
                    + this.baseDirection
                    + "\">"
                    + ((users[i] == this.userName) ? "<b>" + users[i] + "</b>" : users[i])
                    + "</a>"
                    + "<ul class=\"inlineUserMenu\" id=\""
                    + this.getInlineUserMenuDocumentID(this.userMenuCounter, i)
                    + "\" style=\"display:none;\">"
                    + "</ul>";
            }
            this.userMenuCounter++;
            return menu;
        }

        #endregion// Replace Commands

        #region Replace BB Code

        private static Regex regExpStripBBCode;
        private static Regex replaceBBCodeRE;

        private string replaceBBCode(string text)
        {
            if (!this.settings.BBCode)
            {
                // If BBCode is disabled, just strip the text from BBCode tags:
                if (regExpStripBBCode == null)
                {
                    regExpStripBBCode = new Regex(
                        @"\[(?:/)?(\w+)(?:=([^<>]*?))?\]",
                        RegexOptions.Multiline
                    );
                }
                return regExpStripBBCode.Replace(text, "");
            }
            // Remove the BBCode tags:
            if (replaceBBCodeRE == null)
            {
                replaceBBCodeRE = new Regex(
                    @"\[(\w+)(?:=([^<>]*?))?\](.+?)\[/\1\]",
                    RegexOptions.Multiline
                );
            }
            return replaceBBCodeRE.Replace(
                text,
                this.replaceBBCodeCallback
            );
        }

        private string replaceBBCodeCallback(Match match)
        {
            // Only replace predefined BBCode tags:
            string tag = match.Groups[1].Value;
            if (Array.IndexOf(this.bbCodeTags, tag) < 0)
            {
                return match.Groups[0].Value;
            }
            // Avoid invalid XHTML (unclosed tags):
            string content = match.Groups[3].Value;
            if (AjaxChatHelpers.ContainsUnclosedTags(content))
            {
                return match.Groups[0].Value;
            }
            string attribute = match.Groups[2].Value;
            switch (tag)
            {
                case "color":
                    return this.replaceBBCodeColor(content, attribute);
                case "url":
                    return this.replaceBBCodeUrl(content, attribute);
                case "img":
                    return this.replaceBBCodeImage(content);
                case "quote":
                    return this.replaceBBCodeQuote(content, attribute);
                case "code":
                    return this.replaceBBCodeCode(content);
                case "u":
                    return this.replaceBBCodeUnderline(content);
                default:
                    return this.replaceCustomBBCode(tag, attribute, content);
            }
        }

        private string replaceBBCodeColor(string content, string attribute)
        {
            if (this.settings.BBCodeColors)
            {
                // Only allow predefined color codes:
                if (attribute == null || Array.IndexOf(this.colorCodes, attribute) < 0)
                    return content;
                return "<span style=\"color:"
                    + attribute + ";\">"
                    + this.replaceBBCode(content)
                    + "</span>";
            }
            return content;
        }

        private static Regex replaceBBCodeUrlRE;

        private string replaceBBCodeUrl(string content, string attribute)
        {
            string url;
            string encodedSpace = AjaxChatHelpers.EncodeText(" ");
            if (attribute != null)
                url = Regex.Replace(attribute, "\\s", encodedSpace, RegexOptions.Multiline);
            else
                url = AjaxChatHelpers.StripBBCodeTags(Regex.Replace(content, "\\s", encodedSpace, RegexOptions.Multiline));
            if (replaceBBCodeUrlRE == null)
            {
                replaceBBCodeUrlRE = new Regex(
                    "^(?:(?:http)|(?:https)|(?:ftp)|(?:irc))://",
                    RegexOptions.None
                );
            }
            if (url == null || !replaceBBCodeUrlRE.IsMatch(url))
                return content;
            return "<a href=\""
                + url
                + "\" onclick=\"window.open(this.href); return false;\">"
                + this.replaceBBCode(content)
                + "</a>";
        }

        private static Regex replaceBBCodeImageRE;

        private string replaceBBCodeImage(string url)
        {
            if (this.settings.BBCodeImages)
            {
                if (replaceBBCodeImageRE == null)
                {
                    replaceBBCodeImageRE = new Regex(
                        this.regExpMediaURL,
                        RegexOptions.None
                    );
                }
                if (url == null || !replaceBBCodeImageRE.IsMatch(url))
                    return url;
                url = Regex.Replace(url, "\\s", AjaxChatHelpers.EncodeText(" "), RegexOptions.Multiline);
                System.Drawing.Rectangle offsetRect = this.dom["chatList"].OffsetRectangle;
                int maxWidth = offsetRect.Width - 50;
                int maxHeight = offsetRect.Height - 50;
                return "<a href=\""
                    + url
                    + "\" onclick=\"window.open(this.href); return false;\">"
                    + "<img class=\"bbCodeImage\" style=\"max-width:"
                    + maxWidth
                    + "px; max-height:"
                    + maxHeight
                    + "px;\" src=\""
                    + url
                    + "\" alt=\"\" onload=\"ajaxChat.updateChatlistView();\"/></a>";
            }
            return url;
        }

        private string replaceBBCodeQuote(string content, string attribute)
        {
            if (attribute != null)
            {
                return "<span class=\"quote\"><cite>"
                    + this.lang.cite.Replace("%s", attribute)
                    + "</cite><q>"
                    + this.replaceBBCode(content)
                    + "</q></span>";
            }
            return "<span class=\"quote\"><q>"
                + this.replaceBBCode(content)
                + "</q></span>";
        }

        private string replaceBBCodeCode(string content)
        {
            // Replace vertical tabs and multiple spaces with two non-breaking space characters:
            return "<code>"
                + Regex.Replace(content, "\\t|(?:  )", "&#160;&#160;")
                + "</code>";
        }

        private string replaceBBCodeUnderline(string content)
        {
            return "<span style=\"text-decoration:underline;\">"
                + this.replaceBBCode(content)
                + "</span>";
        }

        #endregion// Replace BB Code

        private static Regex replaceHyperLinksRE;

        private string replaceHyperLinks(string text)
        {
            if (!this.settings.HyperLinks)
            {
                return text;
            }
            if (replaceHyperLinksRE == null)
            {
                replaceHyperLinksRE = new Regex(
                    @"(^|\s|>)((?:(?:http)|(?:https)|(?:ftp)|(?:irc))://[^\s<>]+)(</a>)?",
                    RegexOptions.Multiline
                );
            }
            return replaceHyperLinksRE.Replace(text, this.replaceHyperLinksCallback);
        }

        private string replaceHyperLinksCallback(Match match)
        {
            // Do not replace URL's inside URL's:
            if (match.Groups.Count >= 4 && match.Groups[3].Success)
            {
                return match.Groups[0].Value;
            }
            string p2 = match.Groups[2].Value;
            return match.Groups[1].Value
                + "<a href=\""
                + p2
                + "\" onclick=\"window.open(this.href); return false;\">"
                + p2
                + "</a>";
        }

        private static Regex replaceLineBreaksRE;

        private string replaceLineBreaks(string text)
        {
            if (replaceLineBreaksRE == null)
            {
                replaceLineBreaksRE = new Regex("\\n", RegexOptions.None);
            }
            if (!this.settings.LineBreaks)
            {
                return replaceLineBreaksRE.Replace(text, " ");
            }
            else
            {
                return replaceLineBreaksRE.Replace(text, "<br/>");
            }
        }

        private static Regex replaceEmoticonsRE;

        private string replaceEmoticons(string text)
        {
            if (!this.settings.Emoticons)
            {
                return text;
            }
            if (replaceEmoticonsRE == null)
            {
                string regExpStr = "^(.*)(";
                for (int i = 0; i < this.emoticonCodes.Length; i++)
                {
                    if (i != 0)
                        regExpStr += "|";
                    regExpStr += "(?:" + AjaxChatHelpers.EscapeRegExp(this.emoticonCodes[i]) + ")";
                }
                regExpStr += ")(.*)$";
                replaceEmoticonsRE = new Regex(regExpStr, RegexOptions.Multiline);
            }
            return replaceEmoticonsRE.Replace(text, this.replaceEmoticonsCallback);
        }

        private static Regex replaceEmoticonsCallbackRE;

        private string replaceEmoticonsCallback(Match match)
        {
            if (replaceEmoticonsCallbackRE == null)
            {
                replaceEmoticonsCallbackRE = new Regex("(=\"[^\"]*$)|(&[^;]*$)", RegexOptions.None);
            }
            // Avoid replacing emoticons in tag attributes or XHTML entities:
            string p1 = match.Groups[1].Value;
            if (replaceEmoticonsCallbackRE.IsMatch(p1))
            {
                return match.Groups[0].Value;
            }
            if (match.Groups[2].Success)
            {
                string p2 = match.Groups[2].Value;
                int index = Array.IndexOf(this.emoticonCodes, p2);
                return this.replaceEmoticons(p1)
                    + "<img src=\""
                    + this.dirs["emoticons"]
                    + this.emoticonFiles[index]
                    + "\" alt=\""
                    + p2
                    + "\" />"
                    + this.replaceEmoticons(match.Groups[3].Value);
            }
            return match.Groups[0].Value;
        }

        #endregion

        #region Style Functions

        private string getActiveStyle()
        {
            string cookie = this.readCookie(this.sessionName + "_style");
            string style = cookie == null ? cookie : this.getPreferredStyleSheet();
            return style;
        }

        private void initStyle()
        {
            this.styleInitiated = true;
            this.setActiveStyleSheet(this.getActiveStyle());
        }

        private void persistStyle()
        {
            if (this.styleInitiated)
            {
                this.createCookie(this.sessionName + "_style", this.getActiveStyleSheet(), this.cookieExpiration);
            }
        }

        private void setSelectedStyle()
        {
            ComboBox styleSelection = null;
            if (styleSelection != null)
            {
                string style = this.getActiveStyle();
                ComboBox.ObjectCollection styleOptions = styleSelection.Items;
                for (int i = 0; i < styleOptions.Count; i++)
                {
                    if (styleOptions[i].ToString() == style)
                    {
                        styleSelection.SelectedIndex = i;
                        break;
                    }
                }
            }
        }

        private string getSelectedStyle()
        {
            ComboBox styleSelection = null;
            ComboBox.ObjectCollection styleOptions = styleSelection.Items;
            if (styleSelection.SelectedIndex == -1)
            {
                return styleOptions[0].ToString();
            }
            else
            {
                return styleSelection.SelectedItem.ToString();
                //return styleOptions[styleSelection.SelectedIndex].ToString();
            }
        }

        private void setActiveStyleSheet(string title)
        {
            HtmlElement a;
            bool titleFound = false;
            HtmlElementCollection links = document.GetElementsByTagName("link");
            for (int i = 0; i < links.Count; i++)
            {
                a = links[i];
                if (a.GetAttribute("rel").IndexOf("style") != -1 && 
                    !string.IsNullOrEmpty(a.GetAttribute("title")))
                {
                    a.Enabled = false;
                    if (a.GetAttribute("title") == title)
                    {
                        a.Enabled = true;
                        titleFound = true;
                    }
                }
            }
            if (!titleFound && title != null)
                this.setActiveStyleSheet(this.getPreferredStyleSheet());
        }

        private string getActiveStyleSheet()
        {
            HtmlElement a;
            HtmlElementCollection links = document.GetElementsByTagName("link");
            for (int i = 0; i < links.Count; i++)
            {
                a = links[i];
                if (a.GetAttribute("rel").IndexOf("style") != -1 
                    && !string.IsNullOrEmpty(a.GetAttribute("title"))
                    && a.Enabled)
                {
                    return a.GetAttribute("title");
                }
            }
            return null;
        }

        private string getPreferredStyleSheet()
        {
            HtmlElement a;
            HtmlElementCollection links = document.GetElementsByTagName("link");
            for (int i = 0; i < links.Count; i++)
            {
                a = links[i];
                if (a.GetAttribute("rel").IndexOf("style") != -1
                    && a.GetAttribute("rel").IndexOf("alt") == -1
                    && !string.IsNullOrEmpty(a.GetAttribute("title")))
                {
                    return a.GetAttribute("title");
                }
            }
            return null;
        }

        #endregion// Style Functions

        private void switchLanguage(string langCode)
        {
            //window.location.search = "?lang=" + langCode;
        }

        private void createCookie(string name, string value, int days)
        {
            cookie = new Cookie(name, value, this.cookiePath, this.cookieDomain);
            if (days != 0)
            {
                DateTime date = DateTime.Now;
                date += new TimeSpan(days, 0, 0, 0);
                cookie.Expires = date;
                cookie.Secure = this.cookieSecure;
            }
        }

        private string readCookie(string name)
        {
            if (cookie == null)
                return null;
            return cookie.Value;
        }

        private bool isCookieEnabled()
        {
            this.createCookie(this.sessionName + "_cookie_test", "true", 1);
            string cookie = this.readCookie(this.sessionName + "_cookie_test");
            if (cookie != null)
            {
                // Unset the test cookie:
                this.createCookie(this.sessionName + "_cookie_test", "true", -1);
                // Cookie test successful, return true:
                return true;
            }
            return false;
        }

        private void finalized()
        {
            if (this.finalizeFunction != null)
                this.finalizeFunction();

            // Ensure the socket connection is closed on unload:
            if (this.socket != null)
            {
                try
                {
                    this.socket.Close();
                    this.socket = null;
                }
                catch (Exception)
                {
                }
            }
            this.persistSettings();
            this.persistStyle();
            this.customFinalize();
        }

        // Override to perform custom actions on flash initialization:
        protected virtual void initializeCustomFlashInterface()
        {
        }

        // AJAX Chat to SMF Private Messages Integration
        // 2 February 2013 - Peter L Jones
        // Show or hide PMAlertContainer depending on whether
        // there are unread PMs
        private void smfUnreadMessages(bool show)
        {
            if (this.dom["PMAlertContainer"] != null)
            {
                HtmlHelpers.SetStyle(this.dom["PMAlertContainer"], "display", show ? "block" : "none");
            }
        }

        // Override to handle custom info messages
        protected virtual void handleCustomInfoMessage(string infoType, string infoData)
        {
            switch (infoType)
            {
                // AJAX Chat to SMF Private Messages Integration
                // 2 February 2013 - Peter L Jones
                // Currently unused new info message
                case "msgCount":
                    //this.smfPrivateMessages(infoData);
                    break;
                // AJAX Chat to SMF Private Messages Integration
                // 2 February 2013 - Peter L Jones
                // Number of unread PMs from SMF
                case "newCount":
                    this.smfUnreadMessages(int.Parse(infoData) > 0);
                    break;
            }
        }

        // Override to add custom initialization code
        // This method is called on page load
        protected virtual void customInitialize()
        {
            this.addChatBotMessageToChatList("Welcome to The Sims 3 Modding chat.  We are all sick of The Sims 3.  Bring on The Sims 4 already!");
        }

        // Override to add custom finalization code
        // This method is called on page unload
        protected virtual void customFinalize()
        {
        }

        // Override to add custom user menu items:
        // Return a string with list items ( <li>menuItem</li> )
        // encodedUserName contains the userName ready to be used for javascript links
        // userID is only available for the online users menu - not for the inline user menu
        // use (encodedUserName == this.encodedUserName) to check for the current user
        protected virtual string getCustomUserMenuItems(string encodedUserName, int userID)
        {
            return "";
        }

        // Override to parse custom input messages:
        // Return replaced text
        // text contains the whole message
        protected virtual string parseCustomInputMessage(string text)
        {
            return text;
        }

        // Override to parse custom input commands:
        // Return parsed text
        // text contains the whole message, textParts the message split up as words array
        protected virtual string parseCustomInputCommand(string text, string[] textParts)
        {
            return text;
        }

        // Override to replace custom text:
        // Return replaced text
        // text contains the whole message
        protected virtual string replaceCustomText(string text)
        {
            return text;
        }

        // Override to replace custom commands:
        // Return replaced text for custom commands
        // text contains the whole message, textParts the message split up as words array
        protected virtual string replaceCustomCommands(string text, string[] textParts)
        {
            return text;
        }

        // Override to replace custom BBCodes:
        // Return replaced text and call replaceBBCode recursively for the content text
        // tag contains the BBCode tag, attribute the BBCode attribute and content the content text
        // This method is only called for BBCode tags which are in the bbCodeTags list
        protected virtual string replaceCustomBBCode(string tag, string attribute, string content)
        {
            return "<" + tag + ">" + this.replaceBBCode(content) + "</" + tag + ">";
        }

        // Override to perform custom actions on new messages:
        // Return true if message is to be added to the chatList, else false
        protected virtual bool customOnNewMessage(DateTime dateObject, int userID, string userName, int userRole, string messageID, string messageText, int channelID, string ip)
        {
            return true;
        }
    }
}
